/*
* Copyright 2007-2008 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
*/

package cl.fbd.widget.flot

import scala.xml.{NodeSeq, Node, PCData, Text, Unparsed}

import net.liftweb.http.js.JsCmd

/**
 * Serie of data
 */ 

class FlotSerie
{
  val data: List [Pair [Double, Double]] = Nil
  val label : Option [String] = None 
  val lines : Option[FlotLinesOptions] = None
  val points : Option[FlotPointsOptions] = None
  val bars : Option[FlotBarsOptions] = None
  val color : Option [Either [String, Int]] = None
  val shadowSize: Option [Int] = None
}

/**
 * axis options 
 */

class FlotAxisOptions {
  val tickDecimals: Option [Double] = None
  val min: Option [Double] = None
  val max: Option [Double] = None
  val mode: Option [String] = None
  val ticks: List [Double] = Nil //  null or number or ticks array or (fn: range -> ticks array)
  
  /* TODO
    autoscaleMargin: null or number
    labelWidth: null or number
    labelHeight: null or number
    tickSize: number or array
    minTickSize: number or array
    tickFormatter: (fn: number, object -> string) or string
    */
}

/**
 * legend options
 */

class FlotLegendOptions {
  val show: Option [Boolean] = None
  val labelFormatter : Option [String] = None // null or (fn: string -> string)
  val labelBoxBorderColor: Option [String] = None // color
  val noColumns: Option [Int] = None // number
  val position: Option [String] = None // "ne" or "nw" or "se" or "sw"
  val margin: Option [Int] = None // number of pixels
  val backgroundColor: Option [String] = None //  null or color
  val backgroundOpacity: Option [Double] = None // number in 0.0 - 1.0
  val container: Option [String] = None // null or jQuery object
}

/**
 * lines options and points/bars options parent 
 */

class FlotLinesOptions {
  val show: Option [Boolean] = None
  val lineWidth : Option [Int] = None
  val fill : Option [Boolean] = None // TODO: boolean or number
  val fillColor : Option [String] = None 
}

class FlotPointsOptions extends FlotLinesOptions {
  val radius : Option [Int] = None  // TODO
}

class FlotBarsOptions extends FlotLinesOptions {
  val barWidth : Option [Int] = None // TODO
}

/**
 * grid options
 */

class FlotGridOptions {
  val color: Option [String] = None 
  val backgroundColor: Option [String] = None
  val tickColor: Option [String] = None
  val labelMargin: Option [Int] = None
  val coloredAreasColor: Option [String] = None
  val borderWidth: Option [Int] = None
  val clickable: Option [Boolean] = None
  val coloredAreas: Option [String] = None // only (fn: plot area -> array of areas)

  /* TODO
  coloredAreas: array of areas or (fn: plot area -> array of areas)
  */
}

/**
 * Options
 */
 
class FlotOptions {
  val lines : Option [FlotLinesOptions] = None
  val points : Option [FlotPointsOptions] = None
  val legend: Option [FlotLegendOptions] = None
  val xaxis: Option [FlotAxisOptions] = None
  val yaxis: Option [FlotAxisOptions] = None
  val modeSelection: Option [String] = None
  val shadowSize: Option [Int] = None
  val grid: Option [FlotGridOptions] = None
}

/**
 *
 */
 
case class JsFlot (val idPlaceholder : String, val datas : List [FlotSerie], val options : FlotOptions) extends JsCmd
{
  def toJsCmd: String = {
    Flot.renderJs (idPlaceholder, datas, options, None, None, Nil)
  }
}

case class JsFlotWithOverview (val idPlaceholder : String, 
                               val datas : List [FlotSerie], 
                               val options : FlotOptions,
                               val idOverview : String,
                               val optionsOverview : FlotOptions) extends JsCmd
{
  def toJsCmd: String = {
    val jsClearLegend = optionsOverview.legend match {      
      case Some (flotLegendOptions) => {
        flotLegendOptions.container match {
          case Some (phContainer) => "    jQuery(\"#" + phContainer + "\").html (\"\") ;\n"  
          case None => ""
        }
      }
      case None => "" 
    }
    
    jsClearLegend + Flot.renderJs (idPlaceholder, datas, options, Some (idOverview), Some (optionsOverview), Nil)
  }
}

/**
 * 
 */ 

object Flot
{
  /**
   * register the resources with lift (typically in boot)
   */
   
  def init() {  
    import net.liftweb.http.ResourceServer  
   
    ResourceServer.allow({  
      case "flot" :: "jquery.flot.js" :: Nil => true  
      case "flot" :: "excanvas.pack.js" :: Nil => true  
    })  
    println("register flot")  
  }
  
  /**
   * render a Flot Graph (without a overview)   
   */
   
  def render (idPlaceholder : String, 
              datas : List[FlotSerie], 
              options : FlotOptions
              ) : (NodeSeq => NodeSeq) = {
    render (idPlaceholder, datas, options, None, None)
  }
  
  /**
   * render a Flot Graph with a overview for selection.  Javascript is automatically rendered.  
   */
   
  def render (idPlaceholder : String, 
              datas : List[FlotSerie], 
              options : FlotOptions, 
              idOverview : String,
              optionsOverview : FlotOptions
              ) : (NodeSeq => NodeSeq) = {
    render (idPlaceholder, datas, options, Some(idOverview), Some (optionsOverview))
  }
  
  /**
   * render a Flot Graph
   */
   
  private def render (idPlaceholder : String,
                      datas : List[FlotSerie], 
                      options : FlotOptions, 
                      idOverview : Option [String],
                      optionsOverview : Option [FlotOptions]
                      ) : (NodeSeq => NodeSeq) = {
    
    def ret (in : NodeSeq) : NodeSeq = {
      
      val tagScript = in \\ "script"
      val jqueryScript = if (! tagScript.isEmpty) tagScript (0).child else Text ("")
      
      val ieExcanvasPackJs = Unparsed ("<!--[if IE]><script language=\"javascript\" type=\"text/javascript\" src=\"" + net.liftweb.http.S.contextPath + "/classpath/flot/excanvas.pack.js\"></script><![endif]-->") 
      
      //
      <xml:group>
      <head>
        <script type="text/javascript" src="/classpath/flot/jquery.flot.js"></script>
        {ieExcanvasPackJs}
        <script type="text/javascript" charset="utf-8">
{_renderJs (idPlaceholder, datas, options, idOverview, optionsOverview, jqueryScript)}
        </script>
      </head> 
      </xml:group>
    }
      
    ret
  }
  
  /*
   * can be used to generate AJAX response 
   */
  
  def renderJs (
                 idPlaceholder : String, 
                 datas : List [FlotSerie], 
                 options : FlotOptions,
                 idOverview : Option [String],
                 optionsOverview : Option [FlotOptions],
                 jqueryScript : Seq [Node]
               ) = {
                          
    val js = datas match {
      case Nil => "jQuery(\"#" + idPlaceholder + "\").hide () ;" +
                            (idOverview match {
                              case Some (_idOverview) =>  "\njQuery(\"#" + _idOverview + "\").hide () ;"
                              case None => ""
                            })
                            
      case _ => "jQuery(\"#" + idPlaceholder + "\").show () ;" +
                (idOverview match {
                              case Some (_idOverview) =>  "\njQuery(\"#" + _idOverview + "\").show () ;"
                              case None => ""
                            }) +
               "var datas_" + idPlaceholder + " = [\n" +
                renderSeries (datas) + "\n" +
                "] ;\n" +
                "var options_" + idPlaceholder + " = " + renderOptions (options) + "; \n" +  
                "var plot_" + idPlaceholder + 
                " = jQuery.plot(jQuery(" + renderId (idPlaceholder) + 
                "), datas_" + idPlaceholder + 
                ", options_" + idPlaceholder + ");\n" + 
                renderOverview (idPlaceholder, idOverview, optionsOverview) + "\n" + 
                renderJqueryScript (jqueryScript) + "\n"  
    }
             
    js
  }
  
  // generate Javascript inside "document ready" event
  
  private def _renderJs (
                        idPlaceholder : String, 
                        datas : List [FlotSerie], 
                        options : FlotOptions,
                        idOverview : Option [String],
                        optionsOverview : Option [FlotOptions],
                        jqueryScript : Seq [Node]
                       ) = {
                         
    // see http://scala.sygneca.com/faqs/xml  
    
    val js = "/* <![CDATA[ */\n" + 
             initFlot + "\n" + 
             renderJs (idPlaceholder, datas, options, idOverview, optionsOverview, jqueryScript) + 
             endFlot + "\n" +
             "/* ]]> */\n"
    
    Unparsed (js)
  }
  
  private def renderJqueryScript (jqueryScript : Seq [Node]) : String = {
    jqueryScript.foldLeft ("") ( (sz,node) => {
      sz + (node match {
          case net.liftweb.util.PCData (_s) => _s
          case _ => node.toString
        })
    })
  }
   
  //
  
  val initFlot = "jQuery(function () {"
  val endFlot = "});" 
  
  /**
   * render a data value:<br/>
   * [2, 10]
   */
  def renderOneValue (one : Pair [Double, Double]) : String = {
    one match {
      case (Math.NaN_DOUBLE, _) => "null"
      case (_, Math.NaN_DOUBLE) => "null"
      case _one => "[" + _one._1 + ", " + _one._2 + "]"
    }
  }
  
  /**
   * render serie of data:<br/>
   * [2, 10], [5, 12], [11, 2]
   */
  def renderValues (values : List [Pair [Double, Double]]) : String = {
    values match {
      case List (last) => renderOneValue (last)
      case head :: tail => renderOneValue (head) + ", " + renderValues (tail)
    }
  }
  
  /**
   * render one serie:<br />
   * <br />
   * (<br />
   *   label: "<name_label>" <br />
   *   lines: { show: true, fill: true } <br />
   *   bars: { show: true } <br />
   *   points: { show: true } <br />
   *   data: [[2, 10], [5, 12], [11, 2]] <br />
   * ) <br />
   */
   
  def renderOneSerie (data : FlotSerie) : String = {
    val set_label = data.label match {
      case Some (label) => 
        "      label: '" + label + "',\n" ;
      case _ => ""
    }
    
    // 
    val set_lines = data.lines match {
      case None => ""
      case Some (_lines) => "      lines: {" + renderLines (_lines) + "},\n" 
    }
    
    val set_points = data.points match {
      case None => ""
      case Some(_points) => "      points: {" + renderPoints (_points) + "},\n"
    }
    
    val set_bars = data.bars match {
      case None => ""
      case Some(_bars) => "      bars: {" + renderBars (_bars) + "},\n"
    }
    
    val set_color = data.color match {
      case None => ""
      case Some (Left (_color)) => "      color: '" + _color + "',\n"
      case Some (Right (_color)) => "      color: " + _color + ",\n"
    }
    
    val set_shadow_size = data.shadowSize match {
      case None => ""
      case Some (_shadowSize) => "      shadowSize: " + _shadowSize + ",\n"
    }
      
    "    {\n" + 
           set_label +
           set_lines +
           set_points +
           set_bars +
           set_color +
           set_shadow_size +
    "      data: [" + renderValues (data.data) + "]\n" + 
    "    }"
  }
  
  /**
   * render all series: <br />
   * <br />
   * ( <br />
   *   label: "<name_label_1>" <br />
   *   lines:  ... <br />
   *   data: [[2, 10], [5, 12], [11, 2]] <br />
   * ), <br />
   * (<br />
   *   label: "<name_label2>"<br />
   *   data: [[2, 14], [6, 4], [11, 17]]<br />
   * )<br />
   *
   */
  def renderSeries (_datas : List [FlotSerie]) : String = {
    
    _datas match {
      case List(last) => renderOneSerie (last)
      case head :: tail => renderOneSerie (head) + ",\n" + renderSeries (tail)
    }
  }
  
  //
  
  def renderTicks (ticks : List [Double]) : String = {
    "TO-DO"
  }
  
  //
  // min: 0, max: 10, tickDecimals: 0
  // mode: "time",
  // minTickSize: [1, "month"],    // TODO
  //
  def renderAxisOptions (options : FlotAxisOptions) : String = {
    var sz = ""
    var coma = ""
    
    sz = options.min match {
      case None => ""
      case Some (_min) => {coma = ", "; "min: " + _min}
    }
    
    sz += (options.max match {
      case None => ""
      case Some (_max) => {val ret = coma + "max: " + _max; coma = ", "; ret}
    })
    
    sz += (options.tickDecimals match {
      case None => ""
      case Some (_tickDecimals) => {val ret = coma + "tickDecimals: " + _tickDecimals; coma = ", "; ret}
    })
    
    sz += (options.ticks match {
      case Nil => ""
      case List (_one) => {val ret = coma + "ticks: " + _one; coma = ", "; ret}
      case head :: tail => {val ret = coma + "ticks: " + renderTicks (options.ticks); coma = ", "; ret}
    })
    
    sz += (options.mode match {
      case None => ""
      case Some (_mode) => {val ret = coma + "mode: '" + _mode + "'"; coma = ", "; ret}
    })
    
    sz
  }
  
  //
  //    xaxis: { tickDecimals: 0 },
  //    yaxis: { min: 0, max: 10 },
  //
  def renderAxis (axis : String, options : FlotAxisOptions) : String = {
    axis + "axis: {" + renderAxisOptions (options) + "}"  
  }
  
  //
  //
  //
  
  def renderGrid (grid : FlotGridOptions) : String = {
    val sz : StringBuilder = new StringBuilder (200)
    var coma = ""
    
    sz.append (grid.color match {
      case None => ""
      case Some(_color) => {val ret = "color: '" + _color + "'"; coma = ","; ret} 
    })
    
    sz.append (grid.backgroundColor match {
      case None => ""
      case Some(_backgroundColor) => {val ret = coma + "backgroundColor: '" + _backgroundColor + "'"; coma = ","; ret} 
    })
    
    sz.append (grid.tickColor match {
      case None => ""
      case Some(_tickColor) => {val ret = coma + "tickColor: '" + _tickColor + "'"; coma = ","; ret} 
    })
    
    sz.append (grid.labelMargin match {
      case None => ""
      case Some(_labelMargin) => {val ret = coma + "labelMargin: " + _labelMargin; coma = ","; ret} 
    })
    
    sz.append (grid.coloredAreasColor match {
      case None => ""
      case Some(_coloredAreasColor) => {val ret = coma + "coloredAreasColor: '" + _coloredAreasColor + "'"; coma = ","; ret} 
    })
    
    sz.append (grid.borderWidth match {
      case None => ""
      case Some(_borderWidth) => {val ret = coma + "borderWidth: " + _borderWidth; coma = ","; ret} 
    })
    
    sz.append (grid.clickable match {
      case None => ""
      case Some(_clickable) => {val ret = coma + "clickable: " + _clickable; coma = ","; ret} 
    })
    
    sz.append (grid.coloredAreas match {
      case None => ""
      case Some(_coloredAreas) => {val ret = coma + "coloredAreas: " + _coloredAreas; coma = ","; ret} 
    })
    
    sz.toString
  }
   
  //
  //
  //
  
  def renderLegend (legend : FlotLegendOptions) : String = {
    val sz : StringBuilder = new StringBuilder (200)
    var coma = ""
    
    sz.append (legend.labelFormatter match {
      case None => ""
      case Some(_labelFormatter) => {coma = ", "; _labelFormatter} 
    })
    
    sz.append (legend.show match {
      case None => ""
      case Some(_show) => {val ret = coma + "show: " + _show ; coma = ", "; ret} 
    })
    
    sz.append (legend.labelBoxBorderColor match {
      case None => ""
      case Some(_labelBoxBorderColor) => {val ret = coma + "labelBoxBorderColor: " + _labelBoxBorderColor ; coma = ", "; ret} 
    })
    
    sz.append (legend.noColumns match {
      case None => ""
      case Some(_noColumns) => {val ret = coma + "noColumns: " + _noColumns ; coma = ", "; ret} 
    })
    
    sz.append (legend.position match {
      case None => ""
      case Some(_position) => {val ret = coma + "position: " + _position ; coma = ", "; ret} 
    })
    
    sz.append (legend.margin match {
      case None => ""
      case Some(_margin) => {val ret = coma + "margin: " + _margin ; coma = ", "; ret} 
    })
    
    sz.append (legend.backgroundColor match {
      case None => ""
      case Some(_backgroundColor) => {val ret = coma + "backgroundColor: " + _backgroundColor ; coma = ", "; ret} 
    })
    
    sz.append (legend.backgroundOpacity match {
      case None => ""
      case Some(_backgroundOpacity) => {val ret = coma + "backgroundOpacity: " + _backgroundOpacity ; coma = ", "; ret} 
    })
    
    sz.append (legend.container match {
      case None => ""
      case Some(_container) => {val ret = coma + "container: jQuery('#" + _container + "')"; coma = ", "; ret} 
    })
    
    sz.toString     
  }
  
  //
  // lines: { show: true, lineWidth: 1
  //
   
  def renderLines (lines : FlotLinesOptions) : String = {
    val sz = new StringBuilder (200)
    var coma = ""
    
    sz.append (lines.show match {
      case None => ""
      case Some(_show) => {val ret = coma + "show: " + _show; coma = ", "; ret} 
    })

    sz.append (lines.lineWidth match {
      case None => ""
      case Some(_lineWidth) => {val ret = coma + "lineWidth: " + _lineWidth; coma = ", "; ret} 
    })

    sz.append (lines.fill match {
      case None => ""
      case Some(_fill) => {val ret = coma + "fill: " + _fill; coma = ", "; ret} 
    })

    sz.append (lines.fillColor match {
      case None => ""
      case Some(_fillColor) => {val ret = coma + "fillColor: '" + _fillColor + "'"; coma = ", "; ret} 
    })

    sz.toString 
  }
  
  //
  //
  //
  
  def renderPoints (points : FlotPointsOptions) : String = {
    renderLines (points) 
    
    // TO-DO radius
  }
  
  def renderBars (bars : FlotBarsOptions) : String = {
    renderLines (bars) 
    
    // TO-DO rest of bar
  }
  
  //
  // {
  //    lines: { show: true},
  //    points: { show: true}
  //    xaxis: { tickDecimals: 0 },
  //    yaxis: { min: 0, max: 10 },
  //    selection: { mode: "x" } 
  //    legend: { noColumns: 2 },  
  // }
  //
  def renderOptions (options : FlotOptions) : String = {
    var first = true
    
    def finLinea () = {
      val ret = if (! first) ",\n      " else "      "
      first = false
      ret
    }
    
    val set_lines = options.lines match {
      case None => ""
      case Some (_lines) => {first=false; "lines: {" + renderLines (_lines) + "}"}
    }
    
    val set_points = options.points match {
      case None => ""
      case Some (_points) => {finLinea + "points: {" + renderPoints (_points) + "}"}
    }
    
    val set_xaxis = options.xaxis match {
      case None => ""
      case Some (options) => {finLinea + renderAxis ("x", options)}
    }
      
    val set_yaxis = options.yaxis match {
      case None => ""
      case Some (options) => {finLinea + renderAxis ("y", options)}
    }
    
    val set_selection = options.modeSelection match {
      case None => ""
      case Some (mode) => {finLinea + "selection: { mode: '" + mode + "'}"}
    }
      
    val set_legend = options.legend match {
      case None => ""
      case Some (_legend) => {finLinea + "legend: {" + renderLegend (_legend) +  "}"}
    }
    
    val set_shadowSize = options.shadowSize match {
      case None => ""
      case Some (_shadowSize) => {finLinea + "shadowSize: " + _shadowSize}
    }
    
    val set_grid = options.grid match {
      case None => ""
      case Some (_grid) => {finLinea + "grid: {" + renderGrid (_grid) + "}"} 
    }
      
    if (! first)
    {
      "{\n" +
        set_lines +
        set_points  +
        set_xaxis + 
        set_yaxis +
        set_selection +
        set_legend +
        set_shadowSize +
        set_grid +
      "  }"
    }
    else
       "{}" 
  }
  
  def renderId (id : String) : String = {
    "'#" + id + "'"
  }
  
  /**
   * generate Javascript that bind the main graph with an overview graph
   */
  
  private def renderOverview (idPlaceholder : String, idOverview : Option [String], optionsOverview : Option [FlotOptions]) : String = {
    idOverview match {
      case None => ""
      case Some (_idOverview) => optionsOverview match {
        case None => ""
         case Some (_optionsOverview) => renderOverview (idPlaceholder, _idOverview, _optionsOverview)
      }
    }
  }
  
  private def renderOverview (idPlaceholder : String, idOverview : String, optionsOverview : FlotOptions) : String = {
    val sz = new StringBuilder (1000)
    
    sz.append ("  var options_" + idOverview + " = " + renderOptions (optionsOverview) + " ;\n\n") 
    sz.append ("  var plot_" + idOverview +" = jQuery.plot(jQuery('#" + idOverview + "'), datas_" + idPlaceholder + ", options_" + idOverview + ") ; \n\n") 
    
    sz.append ("  var internalSelection = false;\n")
    
    sz.append ("  jQuery('#" + idPlaceholder + "').bind('selected', function (event, area) { \n")
    sz.append ("    plot_" + idPlaceholder + " = jQuery.plot(jQuery('#" + idPlaceholder + "'), datas_" + idPlaceholder + ",\n")
    sz.append ("                jQuery.extend(true, {}, options_" + idPlaceholder + ", {\n")
    sz.append ("                    xaxis: { min: area.x1, max: area.x2 },\n")
    sz.append ("                    yaxis: { min: area.y1, max: area.y2 }\n") 
    sz.append ("                }));\n\n")
        
    sz.append ("    if (internalSelection)\n")
    sz.append ("        return;\n")
    sz.append ("    internalSelection = true;\n")
    sz.append ("    plot_" + idOverview +".setSelection(area);\n")
    sz.append ("    internalSelection = false;\n")
    sz.append ("  });\n\n")
    
    sz.append ("  jQuery('#" + idOverview +"').bind('selected', function (event, area) {\n")
    sz.append ("      if (internalSelection)\n")
    sz.append ("          return;\n")
    sz.append ("      internalSelection = true;\n")
    sz.append ("      plot_" + idPlaceholder + ".setSelection(area);\n")
    sz.append ("      internalSelection = false;\n")
    sz.append ("  });\n")
    
    
    sz.toString
  }
}


